<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
  </head>
  <style>
    body {
      background: rgb(255, 255, 255);
    }
    .g6-tooltip {
      border: 1px solid #e2e2e2;
      border-radius: 4px;
      font-size: 12px;
      color: #545454;
      background-color: rgba(255, 255, 255, 0.9);
      padding: 10px 8px;
      box-shadow: rgb(174, 174, 174) 0px 0px 10px;
    }
    canvas {
      border: 1px solid red;
    }
  </style>
  <body>
    <div id="tip">布局中，请稍候......</div>
    <div id="mountNode"></div>
    <script src="../build/g6.js"></script>
    <script src="./assets/d3-4.13.0.min.js"></script>

    <script>
      const colors = ['#f5222d', '#faad14', '#a0d911', '#13c2c2', '#1890ff', '#b37feb', '#eb2f96'];
      const beginColor = '#5b8c00'; // green
      const endColor = '#ff4d4f'; // red
      d3.json('./assets/data/filtered-trade.json', function(data) {
        const nodes = data.nodes;
        const edges = data.edges;

        const nodeMap = new Map();
        const clusterMap = new Map();
        let cidx = 0;
        nodes.forEach(n => {
          nodeMap.set(n.id, n);
          let region = n.region.split(' ');
          if (n.region === 'East Asia') region = n.region;
          else region = region[region.length - 1];

          if (clusterMap.get(region) === undefined) {
            clusterMap.set(region, cidx);
            cidx++;
          }
          const clusterId = clusterMap.get(region);
          const color = colors[clusterId % colors.length];
          n.style = {
            color,
            fill: color,
            stroke: '#666',
            lineWidth: 0.6,
          };
          n.cluster = clusterId;
          n.importValue = 0;
          n.exportValue = 0;
        });
        // map the value of
        edges.forEach(e => {
          e.id = e.eid;
          if (e.value === '') e.value = 0;
          const v = parseFloat(e.value);
          nodeMap.get(e.source).exportValue += v;
          nodeMap.get(e.target).importValue += v;
        });
        mapValueToProp(nodes, 'exportValue', 'size', [2, 12]);
        const graph = new G6.Graph({
          container: 'mountNode',
          width: 800,
          height: 600,
          layout: {
            type: 'fruchterman',
            maxIteration: 8000,
            gravity: 10,
            clustering: true,
            clusterGravity: 30,
            workerEnabled: true,
          },
          padding: 0,
          fitViewPadding: 0,
          fitView: true,
          linkCenter: true,
          defaultNode: {
            shape: 'circle',
            size: 5,
          },
          defaultEdge: {
            shape: 'quadratic',
          },
          modes: {
            default: [
              'drag-node',
              'zoom-canvas',
              'drag-canvas',
              {
                type: 'tooltip',
                formatText(model) {
                  let name = '';
                  let countries = '';
                  if (model.name) name = model.name + '<br/>';
                  if (model.countries) countries = '<br/>Number of Countries: ' + model.countries;
                  const text =
                    name +
                    'Export Value: ' +
                    model.exportValue +
                    '(1000USD)' +
                    '<br/>Region: ' +
                    model.region +
                    '<br/>Cluster: ' +
                    model.cluster +
                    countries;
                  return text;
                },
                shouldUpdate: e => {
                  return true;
                },
              },
            ],
          },
        });

        graph.on('beforelayout', () => {
          console.log(data.nodes[0].x, data.nodes[0].y, data.nodes[10].x, data.nodes[10].y);
        });
        graph.on('afterlayout', () => {
          const tipDiv = document.getElementById('tip');
          tipDiv.innerHTML = '布局完成！';

          const edgeItems = graph.getEdges();
          edgeItems.forEach(e => {
            const lineWidth = 0.4;
            const strokeOpacity = 0.2;
            let stroke = 'l(0) 0:' + beginColor + ' 1:' + endColor;
            const sourceModel = e.getSource().getModel();
            const targetModel = e.getTarget().getModel();
            if (sourceModel.x > targetModel.x) {
              stroke = 'l(0) 0:' + endColor + ' 1:' + beginColor;
            }
            e.update({
              style: {
                lineWidth,
                strokeOpacity,
                stroke,
              },
            });
          });
          // 等布局完以后再绘制
          graph.paint();
        });

        graph.data(data);
        // 如果使用web worker，graph.render是异步的，立即返回，在web worker里执行布局。
        // 而如果不使用web worker，graph.render是同步的，意味着只有等布局执行完以后，才会执行graph.render后面的代码。
        graph.render();

        graph.on('node:click', e => {
          const targetItem = e.item;
          const model = targetItem.getModel();
          const graphEdges = graph.getEdges();
          const graphNodes = graph.getNodes();
          // click on the cluster node
          if (model.id.substr(0, 7) === 'cluster') {
            graphNodes.forEach(gn => {
              const gnModel = gn.getModel();
              // show the common nodes
              if (gnModel.cluster === model.cluster && gnModel.id.substr(0, 7) != 'cluster') {
                gn.show();
              }
              // remove the cluster nodes
              if (gnModel.id === model.id) graph.removeItem(gn);
            });

            graphEdges.forEach(ge => {
              const edgeModel = ge.getModel();
              const sourceModel = ge.get('sourceNode').getModel();
              const targetModel = ge.get('targetNode').getModel();
              // show the common edges
              if (
                (sourceModel.cluster === model.cluster &&
                  sourceModel.id.substr(0, 7) !== 'cluster') ||
                (targetModel.cluster === model.cluster && targetModel.id.substr(0, 7) !== 'cluster')
              ) {
                ge.show();
                // add the edges to other cluster nodes
                if (!ge.get('sourceNode').get('visible') && sourceModel.cluster !== model.cluster) {
                  let c1 = beginColor,
                    c2 = endColor;
                  if (model.x > targetModel.x) {
                    c1 = endColor;
                    c2 = beginColor;
                  }
                  graph.addItem('edge', {
                    source: 'cluster' + sourceModel.cluster,
                    target: targetModel.id,
                    id: 'cluster-edge-' + edgeModel.id,
                    style: {
                      stroke: 'l(0) 0:' + c1 + ' 1:' + c2,
                      lineWidth: 0.4,
                      strokeOpacity: 0.2,
                    },
                  });
                } else if (
                  !ge.get('targetNode').get('visible') &&
                  targetModel.cluster !== model.cluster
                ) {
                  let c1 = beginColor,
                    c2 = endColor;
                  if (sourceModel.x > model.x) {
                    c1 = endColor;
                    c2 = beginColor;
                  }
                  graph.addItem('edge', {
                    source: sourceModel.id,
                    target: 'cluster' + targetModel.cluster,
                    id: 'cluster-edge-' + edgeModel.id,
                    style: {
                      stroke: 'l(0) 0:' + c1 + ' 1:' + c2,
                      lineWidth: 0.4,
                      strokeOpacity: 0.2,
                    },
                  });
                }
                // hide the edges to the common nodes in other clusters
                if (!ge.get('sourceNode').get('visible') || !ge.get('targetNode').get('visible')) {
                  ge.hide();
                }
              }
              //do not need the following 4 lines, since the related edges will be removed while the cluster node is removing
              // // remove the cluster edges.
              // if (sourceModel.id === model.id || targetModel.id === model.id) {
              //   graph.removeItem(ge);
              // }
            });
          } else {
            // click on the common node, cllapse them
            // calculate the cluster center
            const center = { x: 0, y: 0, count: 0, exportValue: 0 };
            nodes.forEach(n => {
              if (n.cluster === model.cluster) {
                center.x += n.x;
                center.y += n.y;
                center.count++;
                center.exportValue += n.exportValue;
              }
            });
            center.x /= center.count;
            center.y /= center.count;
            // add cluster node on the center
            const size = center.count * 1;
            const clusterNodeId = 'cluster' + model.cluster;
            const color = colors[model.cluster % colors.length];
            const regionStrs = model.region.split(' ');
            let region = regionStrs[regionStrs.length - 1];
            if (model.region === 'East Asia') region = model.region;
            let labelPosition = 'center';
            if (region.length > size) labelPosition = 'left';
            const clusterNode = graph.addItem('node', {
              x: center.x,
              y: center.y,
              id: clusterNodeId,
              cluster: model.cluster,
              region: region,
              countries: center.count,
              exportValue: center.exportValue,
              style: {
                color,
                fill: color,
                stroke: '#666',
                lineWidth: 0.6,
              },
              size,
              label: region,
              labelCfg: {
                style: { fontSize: 8.5 },
                position: labelPosition,
              },
            });

            // add edges about the cluster
            graphEdges.forEach(ge => {
              const edgeModel = ge.getModel();
              const sourceModel = ge.get('sourceNode').getModel();
              const targetModel = ge.get('targetNode').getModel();
              if (!ge.get('sourceNode').get('visible') || !ge.get('targetNode').get('visible'))
                return;
              if (sourceModel.cluster === model.cluster && targetModel.cluster !== model.cluster) {
                let c1 = beginColor,
                  c2 = endColor;
                if (center.x > targetModel.x) {
                  c1 = endColor;
                  c2 = beginColor;
                }
                graph.addItem('edge', {
                  source: clusterNodeId,
                  target: targetModel.id,
                  id: 'cluster-edge-' + edgeModel.id,
                  style: {
                    stroke: 'l(0) 0:' + c1 + ' 1:' + c2,
                    lineWidth: 0.4,
                    strokeOpacity: 0.2,
                  },
                });
              } else if (
                targetModel.cluster === model.cluster &&
                sourceModel.cluster !== model.cluster
              ) {
                let c1 = beginColor,
                  c2 = endColor;
                if (sourceModel.x > center.x) {
                  c1 = endColor;
                  c2 = beginColor;
                }
                graph.addItem('edge', {
                  source: sourceModel.id,
                  target: clusterNodeId,
                  id: 'cluster-edge-' + ge.id,
                  style: {
                    stroke: 'l(0) 0:' + c1 + ' 1:' + c2,
                    lineWidth: 0.4,
                    strokeOpacity: 0.2,
                  },
                });
              }
            });

            // hide the common nodes in the cluster
            graphNodes.forEach(gn => {
              if (
                gn.getModel().cluster === model.cluster &&
                gn.getModel().id.substr(0, 7) !== 'cluster'
              )
                gn.hide();
            });
            // hide the common edges about cluster
            graphEdges.forEach(ge => {
              if (!ge.get('sourceNode').get('visible') || !ge.get('targetNode').get('visible'))
                ge.hide();
            });
          }
        });
      });
      function mapValueToProp(items, valueName, propName, range) {
        const valueRange = [9999999999, -9999999999];
        items.forEach(n => {
          if (n[valueName] > valueRange[1]) valueRange[1] = n[valueName];
          if (n[valueName] < valueRange[0]) valueRange[0] = n[valueName];
        });
        const valueLength = valueRange[1] - valueRange[0];
        const rLength = range[1] - range[0];
        const propNameStrs = propName.split('.');
        if (propNameStrs[0] === 'style' && propNameStrs.length > 1) {
          items.forEach(n => {
            if (n.style === undefined) n.style = {};
            n.style[propNameStrs[1]] =
              (rLength * (n[valueName] - valueRange[0])) / valueLength + range[0];
          });
        } else {
          items.forEach(n => {
            n[propNameStrs[0]] =
              (rLength * (n[valueName] - valueRange[0])) / valueLength + range[0];
          });
        }
      }
    </script>
  </body>
</html>
